package org.voidness.squaretilesframework.server;

import java.util.ArrayList;
import java.util.Collection;

import org.voidness.oje2d.Vector2D;
import org.voidness.squaretilesframework.Map;

/** A class to represent a square tile map */
public class ServerMap extends Map {
    /** The tile information for the map */
    private ServerTile[][] tiles = null;

    /** The map file relative path */
    private String fileName = null;

    /**
     * The default map constructor.
     * 
     * @param mName The name of the map
     * @param mTiles The tile information for the map
     * @param mFileName The relative path of the map file
     */
    public ServerMap(String mName, ServerTile[][] mTiles, String mFileName) {
        super(mName, mTiles, null, null);
        name = mName;
        tiles = mTiles;
        fileName = mFileName;

        // Number of tiles in the map (both in x and y)
        widthInTiles = tiles.length; // horizontal
        heightInTiles = tiles[0].length; // vertical
    }

    /**
     * Another constructor. This one builds an empty map
     * 
     * @param mWidth The width of the new map
     * @param mHeight The height of the new map
     */
    public ServerMap(int mWidth, int mHeight) {
        super(mWidth, mHeight);
        name = new String("untitled");
        widthInTiles = mWidth;
        heightInTiles = mHeight;
        tiles = initializeTiles(mWidth, mHeight);
    }

    /**
     * (Re)initializes the map's tiles
     * 
     * @param mWidth The width of the map
     * @param mHeight The height of the map
     * @return The newly generated tile array
     */
    private ServerTile[][] initializeTiles(int mWidth, int mHeight) {
        ServerTile[][] tiles = new ServerTile[mWidth][mHeight];
        for (int i = 0; i < mWidth; i++)
            for (int j = 0; j < mHeight; j++)
                tiles[i][j] = new ServerTile(this, true);
        return tiles;
    }

    /**
     * (Re)defines the map's name
     * 
     * @param mName The new name to set
     */
    public void setName(String mName) {
        name = mName;
    }

    /**
     * Returns the map's name
     * 
     * @return A string with the map name
     */
    public String getName() {
        return name;
    }

    /**
     * (Re)defines the map's tiles
     * 
     * @param mTiles The new tiles to set
     */
    public void setTiles(ServerTile[][] mTiles) {
        tiles = mTiles;

        // Number of tiles in the map (both in x and y)
        widthInTiles = tiles.length; // horizontal
        heightInTiles = tiles[0].length; // vertical
    }

    /**
     * Defines the tile size.
     * 
     * @param mTileSize The new size to use
     */
    public void setTileSize(int mTileSize) {
        tileSize = mTileSize;
    }

    /**
     * Get the width of the map (in tiles).
     * 
     * @return The width of the map
     */
    public int getWidth() {
        return widthInTiles;
    }

    /**
     * Get the height of the map (in tiles).
     * 
     * @return The height of the map
     */
    public int getHeight() {
        return heightInTiles;
    }

    /**
     * Get the tile at mX,mY.
     * 
     * @param mX The X index of the tile to get
     * @param mY The Y index of the tile to get
     * @return The tile at mX,mY
     */
    public synchronized ServerTile tileAt(int mX, int mY) {
        return tiles[mX][mY];
    }

    /**
     * Get the size of the tiles (in pixels).
     * 
     * @return The tile size
     */
    public int getTileSize() {
        return tileSize;
    }

    /**
     * Returns whether a given location is valid or not
     * 
     * @param mLocation The location to check
     * @return True if it is valid, false otherwise
     */
    public synchronized boolean isValid(Vector2D mLocation) {
        boolean result = mLocation.getX() > -1 && mLocation.getY() > -1;

        if (result)
            result =
                    mLocation.getX() < widthInTiles
                            && mLocation.getY() < heightInTiles;
        else
            return false;

        if (result)
            result = tileAt(mLocation.getX(), mLocation.getY()).isWalkable();
        else
            return false;

        if (result)
            result = !tileAt(mLocation.getX(), mLocation.getY()).isOccupied();
        else
            return false;

        return result;
    }

    /**
     * Returns the nearest valid location
     * 
     * @param mRequested The requested location
     * @return The nearest possible location
     */
    public synchronized Vector2D getNearestValidLocation(Vector2D mRequested) {
        Vector2D validLocation =
                new Vector2D(mRequested.getX(), mRequested.getY());
        Vector2D minValues = new Vector2D(mRequested.getX(), mRequested.getY());
        Vector2D maxValues = new Vector2D(mRequested.getX(), mRequested.getY());

        boolean gotIt = false;
        while (true) {
            for (int i = minValues.getX(); i <= maxValues.getX(); i++)
                if (isValid(new Vector2D(i, minValues.getY()))) {
                    validLocation = new Vector2D(i, minValues.getY());
                    gotIt = true;
                    break;
                }

            if (gotIt)
                break;

            for (int i = minValues.getX(); i <= maxValues.getX(); i++)
                if (isValid(new Vector2D(i, maxValues.getY()))) {
                    validLocation = new Vector2D(i, maxValues.getY());
                    gotIt = true;
                    break;
                }

            if (gotIt)
                break;

            for (int j = minValues.getY() + 1; j <= maxValues.getY() - 1; j++)
                if (isValid(new Vector2D(minValues.getX(), j))) {
                    validLocation = new Vector2D(minValues.getX(), j);
                    gotIt = true;
                    break;
                }

            if (gotIt)
                break;

            for (int j = minValues.getY() + 1; j <= maxValues.getY() - 1; j++)
                if (isValid(new Vector2D(maxValues.getX(), j))) {
                    validLocation = new Vector2D(maxValues.getX(), j);
                    gotIt = true;
                    break;
                }

            if (gotIt)
                break;

            minValues.setX(minValues.getX() - 1);
            minValues.setY(minValues.getY() - 1);
            maxValues.setX(maxValues.getX() + 1);
            maxValues.setY(maxValues.getY() + 1);
        }

        return validLocation;
    }

    /**
     * (Re)defines a map tile's occupation
     * 
     * @param mI The tile's position in the map
     * @param mJ The tile's position in the map
     * @param mOccupier The occupier to set
     */
    public synchronized void setTileOccupier(int mI, int mJ,
            ServerActor mOccupier) {
        tiles[mI][mJ].setOccupier(mOccupier);
    }

    /**
     * Attempts to move the occupier in xPos,yPos to targetX,targetY
     * 
     * @param xPos The current location
     * @param yPos The current location
     * @param targetX The target location
     * @param targetY The target location
     * @return The target location, if successful, false otherwise
     */
    public synchronized Vector2D move(int xPos, int yPos, int targetX,
            int targetY) {
        if (!isValid(new Vector2D(targetX, targetY)))
            return null;

        if (tileAt(targetX, targetY).isWalkable()
                && !tileAt(targetX, targetY).isOccupied()) {
            ServerActor actor = tileAt(xPos, yPos).getServerOccupier();
            tileAt(targetX, targetY).setOccupier(actor);
            tileAt(xPos, yPos).clearOccupation();

            return new Vector2D(targetX, targetY);
        }
        return null;
    }

    /**
     * Returns a list of the characters close to another
     * 
     * @param mActor The actor to get the neighbours
     * @param mRadius The radius to use in the checking
     * @return The list of close characters
     */
    public synchronized Collection<ServerActor> getCloseCharacters(
            ServerActor mActor, int mRadius) {
        Collection<ServerActor> result = new ArrayList<ServerActor>();
        int startX = mActor.getTileX() - mRadius;
        int endX = mActor.getTileX() + mRadius;
        int startY = mActor.getTileY() - mRadius;
        int endY = mActor.getTileY() + mRadius;

        if (startX < 0)
            startX = 0;
        if (endX >= getWidth())
            endX = getWidth() - 1;
        if (startY < 0)
            startY = 0;
        if (endY >= getHeight())
            endY = getHeight() - 1;

        for (int i = startX; i < endX; i++)
            for (int j = startY; j < endY; j++) {
                if (tileAt(i, j).isOccupied()
                        && !tileAt(i, j).getOccupier().equals(mActor))
                    result.add(tileAt(i, j).getServerOccupier());
            }
        return result;
    }

    /**
     * Clones a map.
     * 
     * @return A new map
     */
    public Object clone() {
        return new ServerMap(name, (ServerTile[][]) tiles.clone(), fileName);
    }

    /**
     * Returns the map's relative path
     * 
     * @return A string with the map's filename
     */
    public String getFileName() {
        return fileName;
    }

    /**
     * (Re)defines the map file's path
     * 
     * @param mFileName The file to set
     */
    public void setFileName(String mFileName) {
        fileName = mFileName;
    }
}